package zredis

import (
	"github.com/garyburd/redigo/redis"
	"github.com/gotomicro/ego/core/elog"
	"go.uber.org/zap"
	"sync"
	"time"
)

type LazyPool struct {
	server, password   string
	db                 int
	maxIdle, maxActive int
	pool               *redis.Pool
}

func (p *LazyPool) Get() redis.Conn {
	return &LazyConn{
		LazyPool: p,
		RealPool: p.pool,
	}
}

func (p *LazyPool) report() {
	ticker := time.NewTicker(10 * time.Second)
	for {
		select {
		case <-ticker.C:
			elog.Info("[zredis] pool report", elog.String("server", p.server), elog.Any("Stats", p.pool.Stats()))
		}
	}
}

type LazyConn struct {
	LazyPool *LazyPool
	RealPool *redis.Pool
	RealConn redis.Conn
	Ctime    time.Time
	sync.Mutex
}

func (c *LazyConn) Close() (err error) {
	c.Lock()
	if c.RealConn != nil {
		err = c.RealConn.Close()
		c.RealConn = nil
		c.report()
	}
	c.Unlock()
	return err
}

func (c *LazyConn) Err() (err error) {
	c.Lock()
	if c.RealConn != nil {
		err = c.RealConn.Err()
	}
	c.Unlock()
	return err
}

func (c *LazyConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
	c.Lock()
	defer c.Unlock()
	conn := c.getRealConn()
	t := time.Now()
	reply, err = conn.Do(commandName, args...)
	conn.Close()
	if lt := time.Since(t); lt > 100*time.Millisecond {
		elog.Warn("[zredis] do", zap.String("cmd", commandName), zap.Any("args", args), zap.Duration("life time", lt), zap.StackSkip("stack", 1))
	}
	c.report()
	c.RealConn = nil
	return reply, err
}

func (c *LazyConn) Send(commandName string, args ...interface{}) error {
	c.Lock()
	defer c.Unlock()
	conn := c.getRealConn()
	return conn.Send(commandName, args...)
}

func (c *LazyConn) Flush() error {
	c.Lock()
	defer c.Unlock()
	conn := c.getRealConn()
	return conn.Flush()
}

func (c *LazyConn) Receive() (reply interface{}, err error) {
	c.Lock()
	defer c.Unlock()
	conn := c.getRealConn()
	return conn.Receive()
}

func (c *LazyConn) getRealConn() (conn redis.Conn) {
	if c.RealConn == nil {
		c.RealConn = c.RealPool.Get()
		c.Ctime = time.Now()
	}
	return c.RealConn
}

func (c *LazyConn) report() {
	lt := time.Since(c.Ctime)
	if lt > 300*time.Millisecond {
		elog.Warn("[zredis] connect", zap.Duration("life time", lt), zap.StackSkip("stack", 2))
	}
}

func NewLazyPool(server, password string, db int, maxIdle, maxActive int) *LazyPool {
	lp := &LazyPool{
		server:    server,
		password:  password,
		db:        db,
		maxIdle:   maxIdle,
		maxActive: maxActive,
		pool:      newPool(server, password, db, maxIdle, maxActive),
	}
	go lp.report()
	return lp
}

func newPool(server, password string, db int, maxIdle, maxActive int) *redis.Pool {
	return &redis.Pool{
		MaxIdle:     maxIdle,
		MaxActive:   maxActive,
		IdleTimeout: 240 * time.Second,
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", server)
			if err != nil {
				elog.Error("redis dial", elog.FieldErr(err))
				return nil, err
			}
			if password != "" {
				c.Do("auth", password)
			}
			c.Do("select", db)

			return c, nil
		},
		TestOnBorrow: func(c redis.Conn, t time.Time) error {
			_, err := c.Do("PING")
			return err
		},
	}
}
